Skip to content

Migrate NuGet-backed managers to the V3 API - #5340

Merged
Gabriel Dufresne (GabrielDuf) merged 5 commits into
mainfrom
feat/nuget-v3-api
Sep 1, 2026
Merged

Migrate NuGet-backed managers to the V3 API#5340
Gabriel Dufresne (GabrielDuf) merged 5 commits into
mainfrom
feat/nuget-v3-api

Conversation

@GabrielDuf

Copy link
Copy Markdown
Contributor

Summary

Engineering Manager for NuGet.org reported that UniGetUI's V2/OData traffic consumes a large share of nuget.org's database DTU. This moves the NuGet-backed managers onto the V3 API for feeds that support it, while leaving V2 fully intact for feeds that don't.

What is not migrated, and why

The PowerShell Gallery has no usable V3 service index. Every path under powershellgallery.com/api/v3/ returns 403 — blocked by a Web Application Firewall rule (while /api/v2/ returns 200, and a nonexistent /v3/ returns 404, so the prefix exists and is deliberately blocked). Microsoft's own PSResourceGet docs still register PSGallery as .../api/v2. Step 7 of the issue isn't actionable for it, so both PowerShell managers stay on V2.

Chocolatey stays on V2 too — community.chocolatey.org/api/v3/index.json is a 404. Worth flagging one correction to the issue's framing: Chocolatey is not purely CLI-driven. It derives from BaseNuGet and FindPackages_UnSafe is sealed, so its search, details and icons run through the shared HTTP path; only updates and version listing are choco. V2 is therefore a permanently maintained path, not a fallback to delete later.

A V3-capable custom PowerShell repository (Azure Artifacts, GitHub Packages, JFrog, MyGet) is picked up automatically with no per-manager work.

Risk

Highest-risk area is CompareVersions, because it touches every manager's update decision, not just NuGet. Mitigations: the default preserves current semantics exactly, and there are explicit tests asserting that revision ecosystems still read a trailing suffix as newer — so a future "simplification" that makes the shared parser SemVer-aware will fail CI.

Second: users sitting on a pre-release across eight managers will now be offered the stable release they were previously denied. That's the fix working, but it will look like a surge of new updates.

Behaviour changes users will notice (intended)

  • .NET Tool search returns fewer results — libraries that can't be installed as tools are filtered out
  • Version dropdowns are newest-first, not alphabetical (and no longer truncated: dotnet-ef went from 100 to all 266 versions)
  • Details → Manifest URL is now an api.nuget.org/v3/registration5-…json URL
  • .NET Tool installs now default to global
  • A package on a pre-release may suddenly show an update

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates supported NuGet-backed managers to V3 APIs while retaining V2 compatibility.

Changes:

  • Adds V3 discovery, search, metadata, update, and package-content handling.
  • Introduces ecosystem-specific SemVer and PEP 440 comparisons.
  • Updates .NET Tool defaults, operation history, and regression coverage.

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/UniGetUI.PackageEngine.Tests/SemanticVersionTests.cs Tests semantic-version parsing and ordering.
src/UniGetUI.PackageEngine.Tests/PythonVersionTests.cs Tests PEP 440 behavior.
src/UniGetUI.PackageEngine.Tests/PackageTests.cs Tests manager-specific comparisons.
src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs Tests version-aware cleanup.
src/UniGetUI.PackageEngine.Tests/OperationHistoryTests.cs Tests pinned-version history.
src/UniGetUI.PackageEngine.Tests/NuGetV3ManagerTests.cs Tests V3 manager integration.
src/UniGetUI.PackageEngine.Tests/NuGetV3ClientTests.cs Tests V3 protocol handling.
src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/TestHttpServer.cs Adds concurrent HTTP test support.
src/UniGetUI.PackageEngine.Tests/DotNetManagerTests.cs Tests source and scope changes.
src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs Uses manager-specific comparisons.
src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs Adds default version comparison.
src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/NullPackageManager.cs Implements the comparison contract.
src/UniGetUI.PackageEngine.Operations/PackageOperations.cs Applies ecosystem ordering to cleanup.
src/UniGetUI.PackageEngine.Operations/History/OperationHistoryRecord.cs Records pinned target versions.
src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs Adds PEP 440 comparison.
src/UniGetUI.PackageEngine.Managers.Npm/Npm.cs Adds semantic comparison.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3ServiceIndex.cs Resolves V3 resources.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Json.cs Defines source-generated JSON models.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs Implements V3 requests and caching.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetManifestLoader.cs Generates V3 package URLs.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs Loads V3 details, versions, and icons.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs Routes supported feeds through V3.
src/UniGetUI.PackageEngine.Managers.Dotnet/Helpers/DotNetPkgOperationHelper.cs Defaults tool operations to global scope.
src/UniGetUI.PackageEngine.Managers.Dotnet/DotNet.cs Switches NuGet.org to V3.
src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs Adds semantic comparison.
src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs Adds semantic comparison.
src/UniGetUI.PackageEngine.Interfaces/IPackageManager.cs Exposes version comparison.
src/UniGetUI.Core.Tools/SemanticVersion.cs Implements NuGet-compatible SemVer.
src/UniGetUI.Core.Tools/PythonVersion.cs Implements PEP 440 ordering.
AGENTS.md Documents V3 and comparison architecture.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs Outdated
Comment thread src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs Outdated
Comment thread src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs Outdated
Comment thread src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetManifestLoader.cs Outdated
Comment thread src/UniGetUI.Core.Tools/SemanticVersion.cs Outdated
Comment thread src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs:511

  • If the referenced catalog response is malformed, deserialization throws to the outer catch and returns null, bypassing the nuspec fallback that is used for unavailable registration metadata. A single bad catalog document therefore leaves package details empty even when a valid flat-container nuspec exists. Catch this deserialization failure locally and fall back to GetNuspecMetadata.
                    V3CatalogEntry? entry = NuGetV3Json.DeserializeCatalogEntry(catalogContent);

src/UniGetUI.Core.Tools/SemanticVersion.cs:97

  • SemVer numeric components are not limited to Int32, but this rejects any component above 2,147,483,647. The manager overrides then fall back to VersionStringToStruct, which maps an overflowing segment to zero, so a valid version such as 2147483648.0.0 can compare below 2.0.0 and hide an update. Represent core and numeric prerelease components with arbitrary-precision integers (or compare digit strings by normalized length/value).
                    !int.TryParse(
                        parts[i],
                        NumberStyles.None,
                        CultureInfo.InvariantCulture,
                        out int number

src/UniGetUI.Core.Tools/PythonVersion.cs:95

  • PEP 440 numeric components use arbitrary-precision integers, so rejecting release segments above Int32.MaxValue does not match the claimed reference semantics. Pip then falls back to the shared parser, which maps an overflowing segment to zero; for example, 9999999999.0 can be ordered below 2.0 and suppress a real update. Use arbitrary-precision values for release, epoch, pre/post/dev, and numeric local segments.
                    !int.TryParse(
                        releaseParts[i],
                        NumberStyles.None,
                        CultureInfo.InvariantCulture,
                        out release[i]

src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs:250

  • For an update package, VersionString is the installed version and NewVersionString is the target version. This path therefore loads and displays the old release's manifest, installer URL, dependencies, and date; the V2 path instead caches the GetUpdates entry for the new version (BaseNuGet.cs:613-623). Derive one effective metadata version (NewVersionString when IsUpgradable) and use it for the URLs, catalog fetch, icon lookup, and cache key.
            details.ManifestUrl =
                NuGetV3Client.GetRegistrationLeafUrl(index, package.Id, package.VersionString)
                ?? NuGetV3Client.GetNuspecUrl(index, package.Id, package.VersionString);

            V3CatalogEntry? entry = GetOrFetchCatalogEntry(package, index);

Comment thread src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs Outdated
Comment thread src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.

Suppressed comments (6)

Previously missed (5) — in code that hasn't changed since the last review.

src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs:527

  • If the first copy of an ID has an unparseable version, every later comparison returns null, so that value remains the supposed highest version. The fallback below orders CoreTools.Version.Null below any candidate and can then offer an update already satisfied by a later valid installed copy. Since installed packages support multiple versions and their order is not a highest-first contract, prefer a parseable copy over an unparseable current value before comparing candidates.
                string key = package.Id.ToLower();
                if (
                    !highestInstalledById.TryGetValue(key, out string? highest)
                    || CompareVersions(package.VersionString, highest) > 0
                )

src/UniGetUI.Core.Tools/SemanticVersion.cs:164

  • SemVer numeric identifiers have no 32-bit limit. Once both identifiers exceed Int32.MaxValue, they are treated as strings, so a valid update such as 1.0.0-99999999991.0.0-10000000000 is ordered backwards and may be hidden for npm/Bun/Cargo. Compare digit strings by normalized length and ordinal value instead of parsing them as int.
            bool leftNumeric = int.TryParse(
                left,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out int leftValue

src/UniGetUI.PackageEngine.Operations/History/OperationHistoryRecord.cs:91

  • This treats a whitespace-only option as a pinned version, while the successful install/update paths use IsNullOrWhiteSpace to decide whether an explicit version was requested. Such a record stores whitespace instead of the actual installed version, and retry/undo then reconstructs the package with that invalid version. Apply the same whitespace check here.
                        _ when pop.Options.Version is { Length: > 0 } pinnedVersion =>
                            pinnedVersion,

AGENTS.md:57

  • The documented resource preference list does not match NuGetV3ServiceIndex: search also prefers 3.0.0-rc, and package base address also accepts the unversioned type. Keeping this table exact matters for future feed compatibility work.
| `SearchQueryService` (3.5.0 → 3.0.0-beta → unversioned) | package search |
| `PackageBaseAddress/3.0.0` | version enumeration, update detection, `.nupkg` and embedded-icon URLs |

src/UniGetUI.Core.Tools/PythonVersion.cs:153

  • PEP 440 does not cap epoch, pre/post/dev, or local numeric components at Int32.MaxValue. On overflow this helper silently returns the fallback (usually zero), so a valid version such as 1.0.post2147483648 is parsed as 1.0.post0 and update ordering becomes incorrect. Preserve arbitrary-size numeric values (or compare their digit strings) rather than coercing overflow to zero.
            && int.TryParse(
                group.Value,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out int parsed

src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs:254

  • A service index with only RegistrationsBaseUrl is accepted as usable, but this path treats the missing PackageBaseAddress as a successful empty version response. GetAvailableUpdatesV3 consequently reports every package as current rather than reporting that versions could not be checked. Either enumerate registration data or at least propagate this as a failed check.
            if (index.PackageBaseAddress is null)
                return [];

Comment thread src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs:527

  • When the first installed copy has an unparseable version, comparing a later valid copy against it returns null, so the valid copy never becomes the recorded maximum. The final fallback then treats Version.Null as lower than a candidate and can offer a downgrade even though another installed copy already satisfies it. Apply the existing numeric fallback while reducing installed copies as well.
                string key = package.Id.ToLower();
                if (
                    !highestInstalledById.TryGetValue(key, out string? highest)
                    || CompareVersions(package.VersionString, highest) > 0
                )

src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs:250

  • For an upgradable Package, VersionString is the installed version and NewVersionString is the update target. This V3 path builds the manifest URL and fetches/caches metadata using the installed version, so opening Details on an update shows the old installer URL, dependencies, and publication data. The V2 path preserves the target-version entry in BaseNuGet.cs:603-623; resolve one target metadata version here and use it consistently for details and icon lookup.
            details.ManifestUrl =
                NuGetV3Client.GetRegistrationLeafUrl(index, package.Id, package.VersionString)
                ?? NuGetV3Client.GetNuspecUrl(index, package.Id, package.VersionString);

            V3CatalogEntry? entry = GetOrFetchCatalogEntry(package, index);

src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs:583

  • If a registration leaf is valid but its referenced catalog response is malformed, deserialization throws into this catch and returns null, bypassing the nuspec fallback used for other metadata failures. A feed with a valid nuspec therefore produces empty package details solely because the catalog document is bad; fall back to nuspec metadata here too.
                return null;

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

Previously missed (2) — in code that hasn't changed since the last review.

src/UniGetUI.Core.Tools/SemanticVersion.cs:97

  • SemVer numeric identifiers are not limited to 32-bit values, but this rejects valid core versions above Int32.MaxValue; the manager fallback then maps overflowing legacy segments to zero and can reverse update ordering. Numeric prerelease identifiers in CompareLabel have the same overflow-to-lexical problem. Use arbitrary-precision numeric comparison for both core and prerelease components.
                    !int.TryParse(
                        parts[i],
                        NumberStyles.None,
                        CultureInfo.InvariantCulture,
                        out int number

src/UniGetUI.Core.Tools/PythonVersion.cs:95

  • PEP 440 numeric components have arbitrary precision, but this rejects any valid release segment above Int32.MaxValue. Pip then falls back to the legacy parser, which maps an overflowing segment to zero, so a valid version such as 2147483648.0 can be ordered below 1.0 and hide or invert an update. Represent and compare numeric components without an int limit; the epoch, pre/post/dev, and numeric local fields need the same treatment.
                    !int.TryParse(
                        releaseParts[i],
                        NumberStyles.None,
                        CultureInfo.InvariantCulture,
                        out release[i]

src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs:250

  • For an update package, VersionString is the installed version while NewVersionString is the candidate created by GetAvailableUpdatesV3. This fetches the old version's registration metadata, so the update details dialog shows stale release notes, dependencies, and installer URL while offering the newer version. Use the effective candidate version for upgradable packages and key the metadata/icon caches by that same version.
            details.ManifestUrl =
                NuGetV3Client.GetRegistrationLeafUrl(index, package.Id, package.VersionString)
                ?? NuGetV3Client.GetNuspecUrl(index, package.Id, package.VersionString);

            V3CatalogEntry? entry = GetOrFetchCatalogEntry(package, index);

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉

Integration Details
{
	"deliveryId": "f1ba27c0-a57d-11f1-80a0-a46b344b985f",
	"headSha": "8fb0ed9bcbdc468aa148e2df814444d2c9bee19d",
	"reviewer": "copilot-pull-request-reviewer[bot]"
}

@GabrielDuf
Gabriel Dufresne (GabrielDuf) merged commit b2713c6 into main Sep 1, 2026
9 of 10 checks passed
@GabrielDuf
Gabriel Dufresne (GabrielDuf) deleted the feat/nuget-v3-api branch September 1, 2026 12:10
@GabrielDuf Gabriel Dufresne (GabrielDuf) linked an issue Sep 1, 2026 that may be closed by this pull request
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[ENHANCEMENT] Migrate NuGet V2 APIs to V3 APIs

2 participants